<?php
/*
 * 抖音
 * */
namespace app\common;


use think\facade\Validate;


class DouYin
{
    protected static $clientKey = '';
    protected static $clientSecret = '';

    /**
     * 获取接口统一调用凭据access_token
     *
     * @param  array  $config 抖音配置 ['app_id'=>"app_id",'app_secret'=>"app_secret"]
     * @throws
     * @return array
     */
    public static function getAccessToken(array $config):array{
        $result = [
            'code' => 0,
            'msg' => 'success',
            'data' => ''
        ];

        try{
            if(!isset($config['app_id']) || !isset($config['app_secret'])){
                throw new \Exception('配置参数错误',-1);
            }

            $cache_name = 'douyin_access_token_'.$config['app_id'];

            if(!cache('?'.$cache_name)){

                $url = 'https://developer.toutiao.com/api/apps/v2/token';

                $params = [
                    'appid' => $config['app_id'],
                    'secret' => $config['app_secret'],
                    'grant_type' => 'client_credential',
                ];

                $ret = self::curlPost($url,json_encode($params));
                if(!$ret){
                    throw new \Exception('网络请求失败，稍后再试',550);
                }
                $data = json_decode($ret,true);
                if(!$data || !isset($data['err_no']) || $data['err_no']){
                    throw new \Exception($data['err_tips']??'获取AccessToken未知错误',512);
                }
                $expires_in = $data['data']['expires_in'] ?? 0;//过期时间
                $access_token = $data['data']['access_token'] ?? '';//access token
                cache($cache_name,$access_token,$expires_in);//写入缓存
            }

            $result['data'] = cache($cache_name);
        }catch (\Exception $e){
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }

        return $result;
    }

    /*
     * 取接口调用的凭证 client_access_token
     *
     * */
    public static function getClientToken(string $ck,string $cs){

        $token = cache('douyin_client_token_'.$ck);
        if(!$token){

            //获取access_token

            $url = 'https://open.douyin.com/passport/open/client_token/';

            $param = [
                'client_key'=>$ck,
                'client_secret'=>$cs,
                'grant_type'=> 'client_credential'
            ];

            $header = [
                'Content-Type:multipart/form-data',
            ];

            $redata = self::curlPost($url,json_encode($param),$header);
            $data = json_decode($redata,true);

            if($data && isset($data['data']) && $data['data']['error_code'] == "0"){
                $expires_in = $data['data']['expires_in'] ?? 7200;
                $access_token = $data['data']['access_token'] ?? '';

                cache('douyin_client_token_'.$ck,$access_token,$expires_in);
                $token = $access_token;
            }else{
                file_put_contents('./log/dy/getDouyinClientToken.txt',$redata.PHP_EOL,8);
                return '';
            }
        }

        return $token;
    }

    /**
     * 获取小程序码
     *
     * @param   array $data            [
     * 'width'=>'二维码的宽度，单位 px，最小 280px，最大 1280px',
     * 'line_color'=>'auto_color 为 false 时生效，使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示',
     * ]
     * @param array $config 抖音配置 ['app_id'=>"app_id",'app_secret'=>"app_secret"]
     * @return array
     * 如果调用成功，会直接返回图片二进制内容，如果请求失败，会返回 JSON 格式的数据。
     * */
    public static function getUnlimited(array $data,array $config):array{

        $result = [
            'code' => 0,
            'msg' => 'success',
            'data' => ''
        ];

        //接口请求地址
        $url = "https://developer.toutiao.com/api/apps/qrcode";
        try{

            if(!isset($config['app_id']) || !isset($config['app_secret'])){
                throw new \Exception('配置参数错误',-1);
            }

            $access_token = self::getAccessToken($config);

            if($access_token['code']){
                throw new \Exception($access_token['msg'],$access_token['code']);
            }
            $params =[
                'appname' => 'douyin',
                'access_token' => $access_token['data']
            ];
            $ret = self::curlPost($url,json_encode($params));
            if(!$ret){
                throw new \Exception('网络请求失败，稍后再试',550);
            }
            $data = json_decode($ret,true);
            if(isset($data['errcode'])){
                throw new \Exception($data['errmsg'] ?? '未知错误',$data['errcode'] ?? 500);
            }

            $result['data'] = $ret;
        }catch (\Exception $e){
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }
        return $result;
    }

    /**
     * 获取小程序session key
     *
     * @param   string $code        登录凭证
     * @param   string $appId       应用id
     * @param   string $appSecret   应用秘钥
     * @param   string $anonymous_code  匿名登录凭证
     * @return  array
     * */
    public static function getSessionKey(string $appId,string $appSecret,string $code,string $anonymous_code=''):array{
        $result = [
            'code' => 0,
            'msg' => 'success',
            'data' => []
        ];

        try{
            if(!$code){
                throw new \Exception('无效访问',-2);
            }

            $params = [
                'appid' => $appId,
                'secret' => $appSecret,
                'code' => $code,
                'anonymous_code' =>$anonymous_code
            ];
            //接口请求地址
            $url = "https://developer.toutiao.com/api/apps/v2/jscode2session";
            $ret = self::curlPost($url,json_encode($params));
            if(!$ret){
                throw new \Exception('网络请求失败，稍后再试',550);
            }
            $data = json_decode($ret,true);
            if(!$data || !isset($data['err_no']) || $data['err_no']){
                throw new \Exception($data['err_tips'] ?? '',$data['err_no'] ?? 500);
            }

            $result['data'] = [
                'openid' => $data['data']['openid'] ?? '',
                'session_key' => $data['data']['session_key'] ?? '',
                'anonymous_openid' => $data['data']['session_key'] ?? '',
                'unionid' => $data['data']['unionid'] ?? '',
            ];
        }catch (\Exception $e){
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }

        return $result;
    }

    /**
     * 敏感信息解密
     *
     * @param   string $encryptedData   加密敏感信息
     * @param   string $iv              解密key
     * @param   string $session_key     登录session key
     * @return  array
     * */
    public static function deCode(string $encryptedData,string $iv,string $session_key):array{

        $result = [
            'code' => 0,
            'msg' => 'success',
            'data' => ''
        ];

        try{
            $data = self::decryptData($session_key,$encryptedData,$iv);
            if(!$data){
                throw new \Exception('解密失败',500);
            }

            $res = json_decode($data,true);
            if(!$res){
                throw new \Exception('解密失败',500);
            }

            $result['data'] = $res;

        }catch (\Exception $e){
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }
        return $result;
    }

    /**
     * 敏感信息解密2
     * @param string $sessionKey
     * @param string $encryptedData
     * @param string $iv
     *
     * @return string
     * */
    public static function decryptData(string $sessionKey,string $encryptedData,string $iv):string
    {

        $aesKey = base64_decode($sessionKey);

        $aesIV = base64_decode($iv);

        $aesCipher = base64_decode($encryptedData);

        return openssl_decrypt( $aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV);

    }


    /**
     * 支付
     * @param array $param 支付参数 ['app_id'=>"*app id",'key'=>"*支付加密key",'out_order_no'=>"*订单号",'total_amount'=>*支付金额,'body'=>"*商品详情",'subject'=>"商品描述",'valid_time'=>过期时间（秒）]
     *
     * @return  array
     * */
    public static function pay(array $param):array{
        $result = [
            'code' => 0,
            'msg' => 'success',
            'data' => []
        ];

        //验证字段
        $rule = [
            'app_id'  =>  'require|max:32',//app id
            'key'  =>  'require|max:64',//支付key
            'out_order_no'  =>  'require|max:64',//订单号
            'total_amount'  =>  'require|number|between:1,10000000000',//支付总金额（分）
            'subject'  =>  'require|max:128',//商品描述
            'body'  =>  'require|max:128',//商品详情
            'valid_time'  =>  'require|number|between:300,172800',//过期时间（秒）
            'cp_extra'  =>  'max:2048',//自定义字段值
            'notify_url'  =>  'url|max:256',//支付回调通知地址
            'thirdparty_id'=>'max:64',//第三方平台服务商 id 服务商模式接入必传
            'store_uid'=>'max:64',//多门店模式下可传，可用此字段指定本单使用的收款商户号（目前为灰度功能，需要联系平台运营添加白名单；未在白名单的小程序，默认使用该小程序下尾号为0的商户号收款）
            'disable_msg'=>'number',//是否屏蔽支付完成后推送用户抖音消息，1-屏蔽 0-非屏蔽，默认为0。 特别注意： 若接入POI, 请传1。因为POI订单体系会发消息，所以不用再接收一次担保支付推送消息
            'msg_page'=>'max:1024',//担保支付消息跳转页
            'pay_scene'=>'',//指定支付场景
        ];

        try{

            $validate = Validate::rule($rule);
            if(!$validate->check($param)){
                throw new \Exception($validate->getError(),-1);
            }

            $params = [];

            foreach ($rule as $k=>$v){
                if($k === 'key'){
                    continue;
                }
                if(isset($param[$k]) && $param[$k]){
                    $params[$k] = $param[$k];
                }
            }

            //②、统一下单WxPayUnifiedOrder
            $params = array_filter($params);
            $params['sign'] = self::sign($params,$param['key']);

            //接口请求地址
            $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_order';
            $ret = self::curlPost($url,json_encode($params));
            if(!$ret){
                throw new \Exception('网络请求失败，稍后再试',550);
            }
            $data = json_decode($ret,true);
            if(!$data || $data['err_no']){
                throw new \Exception($data['err_tips'] ?? '支付系统错误',$res['err_no'] ?? 500);
            }
            $data['data']['pay_type'] = 'toutiao';
            $result['data'] = $data['data'];
        }catch(\Exception $e) {
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }
        return $result;
    }

    /**
     * 支付结果查询
     *
     * @param string $appId app id
     * @param string $outOrderNo 商户订单号
     * @param string $salt 支付salt
     *
     * @return bool
     *
     * */
    public static function payQuery(string $appId,string $outOrderNo,string $salt):bool{

        try{

            $params = [
                'app_id' => $appId,
                'out_order_no' => $outOrderNo,
            ];

            $params['sign'] = self::sign($params,$salt);

            //接口请求地址
            $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_order';
            $ret = self::curlPost($url,json_encode($params));
            if(!$ret){
                throw new \Exception('网络请求失败，稍后再试',550);
            }
            $data = json_decode($ret,true);
            if(!$data || $data['err_no']){
                throw new \Exception($data['err_tips'] ?? '支付系统错误',$res['err_no'] ?? 500);
            }

        }catch(\Exception $e) {
            return false;
        }
        return true;
    }



    /**
     * 退款
     * @param array $param 退款参数 ['app_id'=>"*app id",'key'=>"*支付加密key",'out_order_no'=>"*订单号",'refund_amount'=>*退款总金额,'out_refund_no'=>"*退款订单号",'reason'=>"退款原因"]
     *
     * @return  array
     * */
    public static function refund(array $param):array{
        $result = [
            'code' => 0,
            'msg' => 'success',
            'data' => []
        ];

        //验证字段
        $rule = [
            'app_id'  =>  'require|max:64',//app id
            'key'  =>  'require|max:64',//支付key
            'out_order_no'  =>  'require|max:64',//支付订单号
            'out_refund_no'  =>  'require|max:64',//退款订单号
            'refund_amount'  =>  'require|number|between:1,10000000000',//退款总金额（分）
            'reason' => 'require|max:100',//退款原因
            'cp_extra'  =>  'max:2048',//自定义字段值
            'notify_url'  =>  'url|max:256',//支付回调通知地址
            'thirdparty_id'=>'max:64',//第三方平台服务商 id 服务商模式接入必传
            'disable_msg'=>'number',//是否屏蔽支付完成后推送用户抖音消息，1-屏蔽 0-非屏蔽，默认为0。 特别注意： 若接入POI, 请传1。因为POI订单体系会发消息，所以不用再接收一次担保支付推送消息
            'msg_page'=>'max:1024',//担保支付消息跳转页
        ];

        try{

            $validate = Validate::rule($rule);
            if(!$validate->check($param)){
                throw new \Exception($validate->getError());
            }

            $params = [];
            foreach ($rule as $k=>$v){
                if($k === 'key'){
                    continue;
                }
                if(isset($param[$k]) && $param[$k]){
                    $params[$k] = $param[$k];
                }
            }

            //②、统一下单WxPayUnifiedOrder
            $params = array_filter($params);
            $params['sign'] = self::sign($params,$param['key']);

            //接口请求地址
            $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_refund';
            $ret = self::curlPost($url,json_encode($params));
            if(!$ret){
                throw new \Exception('网络请求失败，稍后再试',550);
            }
            $data = json_decode($ret,true);
            if(!$data || $data['err_no']){
                throw new \Exception($data['err_tips'] ?? '退款系统错误',$res['err_no'] ?? 500);
            }
            $result['data'] = $data['refund_no'];
        }catch(\Exception $e) {
            $result['code'] = 1;
            $result['msg'] = $e->getMessage();
        }
        return $result;
    }

    /**
     * 退款结果查询
     *
     * @param string $appId app id
     * @param string $outRefundNo 商户退款单号
     * @param string $salt 支付salt
     *
     * @return bool
     *
     * */
    public static function refundQuery(string $appId,string $outRefundNo,string $salt):bool{

        try{

            $params = [
                'app_id' => $appId,
                'out_refund_no' => $outRefundNo,
            ];
            $params['sign'] = self::sign($params,$salt);

            //接口请求地址
            $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_refund';
            $ret = self::curlPost($url,json_encode($params));
            file_put_contents('dy_refund.txt',$ret.PHP_EOL,8);
            if(!$ret){
                throw new \Exception('网络请求失败，稍后再试',550);
            }
            $data = json_decode($ret,true);
            if(!$data || $data['err_no']){
                throw new \Exception($data['err_tips'] ?? '支付系统错误',$res['err_no'] ?? 500);
            }

        }catch(\Exception $e) {
            return false;
        }
        return true;
    }


    /**
     * 验券准备
     *
     * @param string $encrypted_data
     * @param string $code
     *
     * @return array
     *
     * */
    public static function prepare(string $encrypted_data='',string $code=''){
        $url = 'https://open.douyin.com/goodlife/v1/fulfilment/certificate/prepare?';
        if($encrypted_data){
            $url .= 'encrypted_data='.$encrypted_data;
        }else{
            $url .= 'code='.$code;
        }

        $header = [
            'Content-Type:application/json',
            'access-token:'.self::getClientToken()
        ];

        $redata = self::curlPost($url,'',$header);
        //file_put_contents('./log/dy/douyin_prepare'.date('Y-m-dH').'.txt',date('Y-m-d H:i:s').$redata.PHP_EOL,8);
        $resdata = json_decode($redata,true);

        $result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []//返回的数据，array or string
        ];

        try{
            if(!$resdata || !isset($resdata['data'])){
                throw new \Exception('网络错误');
            }

            $data = $resdata['data'];

            if($data['error_code']){
                throw new \Exception($data['description'] ?? '');
            }


            $coupons = $data['certificates'] ?? null;
            $result['data'] = [
                'verify_token' => $data['verify_token'] ?? '',
                'order_id' => $data['order_id'] ?? '',
                'coupon' => array_shift($coupons)
            ];

        }catch (\Exception $e){
            $result['code'] = 1;
            $result['msg'] = $e->getMessage();
        }

        return $result;
    }

    /**
     * 核销券码
     *
     * @param array $param = [
    'verify_token' => $verify_token,
    'poi_id' => 1111,
    'encrypted_codes|codes|code_with_time_list'=>['']
    ];
     *
     *
     * @return array
     * */
    public static function verify(array $param){
        $url = 'https://open.douyin.com/goodlife/v1/fulfilment/certificate/verify';

        $header = [
            'Content-Type:application/json',
            'access-token:'.self::getClientToken()
        ];

        $redata = self::curlPost($url,json_encode($param),$header);
        //file_put_contents('./log/dy/douyin_verify'.date('Y-m-dH').'.txt',date('Y-m-d H:i:s').$redata.PHP_EOL,8);
        $resdata = json_decode($redata,true);

        $result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []//返回的数据，array or string
        ];

        try{
            if(!$resdata || !isset($resdata['data'])){
                throw new \Exception('网络错误');
            }

            $data = $resdata['data'];

            if($data['error_code']){
                throw new \Exception($data['description'] ?? '');
            }

            $result['data'] = array_shift($data['verify_results']);

        }catch (\Exception $e){
            $result['code'] = 1;
            $result['msg'] = $e->getMessage();
        }


        return $result;
    }

    /**
     * 请求签名算法
     * @param array $map 参数数据
     * @param string $salt 加密salt
     * @return string
     * */
    public static function sign(array $map,string $salt):string {
        $rList = [];
        foreach($map as $k =>$v) {
            if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id"){
                continue;
            }
            $value = trim(strval($v));
            $len = strlen($value);
            if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\""){
                $value = substr($value,1, $len-1);
            }
            $value = trim($value);
            if ($value == "" || $value == "null") {
                continue;
            }

            $rList[] = $value;
        }

        $rList[] = $salt;

        sort($rList, 2);

        return strtolower(md5(implode('&', $rList)));
    }

    /**
     * 回调签名算法
     *
     * @param array $map 参数数据
     * @param string $token 加密token
     * @return string
     * */
    public static function deSign(array $map,string $token):string{
        $rList = [];
        foreach($map as $k =>$v) {
            if ($k == "type" || $k == "msg_signature"){
                continue;
            }
            $value = trim(strval($v));
            $len = strlen($value);
            if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\""){
                $value = substr($value,1, $len-1);
            }
            $value = trim($value);
            if ($value == "" || $value == "null") {
                continue;
            }

            $rList[] = $value;
        }

        $rList[] = $token;

        sort($rList, 2);

        return sha1(implode('', $rList));
    }

    /**
     * curl post提交
     *
     * @param   string  $url
     * @param   string  $data
     *
     * @return  string|bool
     */
    public static function curlPost(string $url,string $data,$head=[]){
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);

        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        $header = count($head)?$head:[
            'Content-Type:application/json; charset=utf-8',
            'Content-Length:' .strlen($data)
        ];
        curl_setopt($curl, CURLOPT_HEADER, 0);
        curl_setopt($curl,CURLOPT_HTTPHEADER,$header);



        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($curl);
        curl_close($curl);
        return $output;
    }

}

